package game;

import java.util.*;

import game.buildings.*;
import game.resources.*;
import game.saints.*;

/**
 *	@(#)Player.java
 *
 *	The <code>Player</code> class represents an Antiquity player.
 *	It provides all information on a player's achievements in game.
 *
 *	@author Grzegorz Kobiela
 *	@version 1.0 2008/5/10
 */
public final class Player
extends GameObject
implements Colored
{
	/** The player's name. */
	private String name;

	/** The player's color. */
	private PlayerColor color;

	/** The player's citymaps. */
	private ArrayList<Citymap> citymaps;

	/** The player's buildings. */
	private ArrayList<Building> buildings;

	/** The player's buildings. */
	private ArrayList<Worker> workers;

	/** The player's zone of control. */
	private ArrayList<Hexfield> zoc;

	/** The player's resources. */
	private Hashtable<ResourceType, ArrayList<Resource>> resources;

	/** The player's saint. */
	private Saint saint = null;

	/** The graves a player gets in the current round. */
	private int graves = 0;

	/** The city space left. */
	private int space = 0;

	/** True if the player has won. */
	private boolean won = false;

	/**
	 *	Creates a new <code>Player</code> instance with the given name and color.
	 *	@param name the name
	 *	@param color the color
	 */
	public Player(String name, PlayerColor color) {
		this.name = name;
		this.color = color;
		citymaps = new ArrayList<Citymap>();
		buildings = new ArrayList<Building>();
		workers = new ArrayList<Worker>();
		zoc = new ArrayList<Hexfield>();
		resources = new Hashtable<ResourceType, ArrayList<Resource>>();
	}

	/**
	 *	Adds a new citymap.
	 *	@param citymap the citymap
	 */
	public void addCitymap(Citymap citymap) {
		citymaps.add(citymap);
	}

	/**
	 *	Returns all citymaps.
	 *	@return all citymaps
	 */
	public ArrayList<Citymap> getCitymaps() {
		return citymaps;
	}

	/**
	 *	Adds the given building.
	 *	@param building the building
	 */
	public void addBuilding(Building building) {
		buildings.add(building);
	}

	/**
	 *	Returns all buildings.
	 *	@return all buildings
	 */
	public ArrayList<Building> getBuildings() {
		return buildings;
	}

	/**
	 *	Returns all buildings of the given type.
	 *	@param type the type
	 *	@return all buildings of the given type
	 */
	public <T extends Building> ArrayList<T> getBuildings(Class<T> type) {
		ArrayList<T> ret = new ArrayList<T>();
		for(Building building: buildings) if(type.isInstance(building)) ret.add(type.cast(building));
		return ret;
	}

	/**
	 *	Returns the amount of buildings of the given type.
	 *	@param type the type
	 *	@return the amount of buildings of the given type
	 */
	public <T extends Building> int hasBuilding(Class<T> type) {
		ArrayList<T> buildings = getBuildings(type);
		int n = 0;
		for(Building building: buildings) {
			if(!building.isActive()) continue;
			if(building instanceof Mannable && !((Mannable)building).isManned()) continue;
			if(building instanceof CityBuilding && (!((CityBuilding)building).isEnabled() || ((CityBuilding)building).isUsed())) continue;
			n++;
		}
		return n;
	}

	/**
	 *	Removes the given building.
	 *	@param building the building
	 */
	public void removeBuilding(Building building) {
		buildings.remove(building);
		building.deactivate();
	}

	/**
	 *	Adds a new worker.
	 *	@param worker the worker
	 */
	public void addWorker(Worker worker) {
		workers.add(worker);
	}

	/**
	 *	Returns all workers.
	 *	@return all workers
	 */
	public ArrayList<Worker> getWorkers() {
		return workers;
	}

	/**
	 *	Adds the given resource.
	 *	@param resource the resource
	 */
	public void addResource(Resource resource) {
		ResourceType type = resource.getType();
		if(!resources.containsKey(type)) resources.put(type, new ArrayList<Resource>());
		resources.get(type).add(resource);
	}

	/**
	 *	Returns all resources.
	 *	@return all resources
	 */
	public ArrayList<Resource> getResources() {
		ArrayList<Resource> ret = new ArrayList<Resource>();
		for(ArrayList<Resource> r: resources.values()) ret.addAll(r);
		return ret;
	}

	/**
	 *	Returns all resources of the given type.
	 *	@param type the resource type
	 *	@return all resources
	 */
	public ArrayList<Resource> getResources(ResourceType type) {
		if(!resources.containsKey(type)) resources.put(type, new ArrayList<Resource>());
		return resources.get(type);
	}

	/**
	 *	Removes the given resource.
	 *	@param resource the resource
	 */
	public void removeResource(Resource resource) {
		ResourceType type = resource.getType();
		if(!resources.containsKey(type)) return;
		resources.get(type).remove(resource);
		resource.deactivate();
	}

	/**
	 *	Returns the amount of resources compatible to the provided resource class.
	 *	@param resource the resource class
	 *	@return the amount of resources
	 */
	public int hasResource(Class<? extends Resource> resource) {
		return hasResource(resource, true);
	}

	/**
	 *	Returns the amount of resources compatible to the provided resource class.
	 *	@param resource the resource class
	 *	@param different provide true if food and luxury goods have to be different
	 *	@return the amount of resources
	 */
	public int hasResource(Class<? extends Resource> resource, boolean different) {
		if(resource == Food.class) {
			int n = 0;
			n += hasResource(ResourceType.FISH, different);
			n += hasResource(ResourceType.GRAIN, different);
			n += hasResource(ResourceType.OLIVES, different);
			n += hasResource(ResourceType.SHEEP, different);
			return n;
		} else if(resource == LuxuryGood.class) {
			int n = 0;
			n += hasResource(ResourceType.DYE, different);
			n += hasResource(ResourceType.GOLD, different);
			n += hasResource(ResourceType.PEARLS, different);
			n += hasResource(ResourceType.WINE, different);
			return n;
		} else {
			try {
				return hasResource(resource.newInstance().getType(), different);
			} catch(Exception e) {
				throw new RuntimeException("Failed to determine resource count of '" + resource + "' in Player.hasResource(Class<? extends Resource>)!");
			}
		}
	}

	/**
	 *	Returns the amount of resources of the given type.
	 *	@param type the resource type
	 *	@param different provide true if food and luxury goods have to be different
	 *	@return the amount of resources
	 */
	private int hasResource(ResourceType type, boolean different) {
		if(!resources.containsKey(type)) return 0;
		int n = resources.get(type).size();
		if(!different || type.equals(ResourceType.WOOD) || type.equals(ResourceType.STONE) || hasBuilding(Philosophy.class) > 0) return n;
		return Math.min(n, 1);
	}

	/**
	 *	Sets the amount of food missing in the current round.
	 *	@param food the amount of food missing
	 */
	public void starve(int food) {
		graves = food;
	}

	/**
	 *	Returns the amount of food missing in the current round.
	 *	@return the amount of food missing
	 */
	public int starves() {
		return graves;
	}

	/**
	 *	Returns the city space left.
	 *	@return the city space left
	 */
	public int getSpace() {
		return space;
	}

	/**
	 *	Changes the city space by the given value.
	 *	If zero is provided it will be set to zero.
	 *	@param value the new value
	 */
	public void updateSpace(int value) {
		if(value != 0) {
			space += value;
		} else {
			space = 0;
		}
	}

	/**
	 *	Returns the city grid array.
	 *	@return the grid
	 */
	public GameObject[][] createGrid() {
		int n = 0;
		for(Citymap citymap: getCitymaps()) if(citymap.isActive()) n++;
		GameObject[][] grid = new GameObject[7 * n][7];
		for(GameObject[] subgrid: grid) Arrays.fill(subgrid, EmptySpace.getInstance());
		for(int i = 1; i < n; i++) {
			for(int j = 0; j < 7; j++){
				grid[7 * i + j][0] = null;
				grid[7 * i][j] = null;
			}
		}
		return grid;
	}

	/**
	 *	Returns the name of the player.
	 *	@return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 *	Returns the color of the player.
	 *	@return the color
	 */
	public PlayerColor getColor() {
		return color;
	}

	/**
	 *	Returns true if the player has won.
	 *	@return true if the player has won
	 */
	public boolean wins() {
		return won;
	}

	/**
	 *	Sets the player to be a winner.
	 */
	public void win() {
		won = true;
	}

	/**
	 *	Returns the saint of the player.
	 *	@return the saint
	 */
	public Saint getSaint() {
		return saint;
	}

	/**
	 *	Sets the saint of the player.
	 *	@param saint the saint
	 */
	public void setSaint(Saint saint) {
		this.saint = saint;
	}

	/**
	 *	Returns true if the player has Santa Barbara's special ability.
	 *	@return true if the player has Santa Barbara's special ability
	 */
	public boolean hasBarbaraAbility() {
		return hasAbility(Barbara.getInstance());
	}

	/**
	 *	Returns true if the player has San Christofori's special ability.
	 *	@return true the if player has San Christofori's special ability
	 */
	public boolean hasChristoforiAbility() {
		return hasAbility(Christofori.getInstance());
	}

	/**
	 *	Returns true if the player has San Giorgio's special ability.
	 *	@return true the if player has San Giorgio's special ability
	 */
	public boolean hasGiorgioAbility() {
		return hasAbility(Giorgio.getInstance());
	}

	/**
	 *	Returns true if the player has San Nicolo's special ability.
	 *	@return true the if player has San Nicolo's special ability
	 */
	public boolean hasNicoloAbility() {
		return hasAbility(Nicolo.getInstance());
	}

	/**
	 *	Returns true if the player has Santa Maria's abilities.
	 *	@return true if the player has Santa Maria's abilities
	 */
	public boolean hasAllAbilities() {
		return hasAbility(Maria.getInstance());
	}

	/**
	 *	Checks if the player has the given ability.
	 *	@return true if the player has the given ability
	 */
	private boolean hasAbility(Saint saint) {
		return (((CityBuilding)getBuildings(Cathedral.class).get(0)).isEnabled() && (this.saint == saint || this.saint == Maria.getInstance()));
	}

	/**
	 *	Returns the list of hexfields in the player's zone of control.
	 *	@return the list of hexfields
	 */
	public ArrayList<Hexfield> getZoneOfControl() {
		return zoc;
	}

	/**
	 *	Updates the list of hexfields in the player's zone of control.
	 *	@param board the game board object
	 */
	public void updateZoneOfControl(GameBoard board) {
		zoc.clear();
		int depth = (hasBuilding(Stables.class) > 0 ? 3 : 2);
		boolean water = (hasBuilding(Harbour.class) > 0);
		Hexfield hex;
		for(Building building: getBuildings(City.class)) {
			hex = board.getHexfield(((City)building).getLocation());
			board.findZoneOfControl(zoc, hex, null, depth + 1, depth - 1, water);
		}
		for(Building building: getBuildings(Inn.class)) {
			hex = board.getHexfield(((Inn)building).getLocation());
			board.findZoneOfControl(zoc, hex, null, depth, depth - 1, water);
		}
		for(int i = 0; i < zoc.size();) {
			hex = zoc.get(i);
			if(hex.isDisabled()) {
				zoc.remove(hex);
			} else {
				i++;
			}
		}
	}
}